﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace GameToolkit
{

    [System.Serializable]
    public struct MaterialValue : System.IEquatable<MaterialValue>
    {
        public EShaderPropertyType type;
        public string name;
        [System.NonSerialized]
        public int id;
        public Vector4 value;

        public MaterialValue(EShaderPropertyType type, string name)
        {
            this.name = name;
            this.id = Shader.PropertyToID(name);
            value = Vector4.zero;
            this.type = type;
        }

        public void UpdateId()
        {
            id = string.IsNullOrEmpty(name) ? 0 : Shader.PropertyToID(name);
        }

        public float FloatValue { get { return value.x; } set { this.value.x = value; } }

        public Color ColorValue { get { return value; } set { this.value = value; } }

        public int IntegerValue { get { return (int)value.x; } set { this.value.x = value; } }

        public bool Equals(MaterialValue other)
        {
            return this.id == other.id && this.type == other.type && Mathf.Approximately(0, Vector4.SqrMagnitude(this.value - other.value));
        }
    }

    [System.Serializable, System.Obsolete("不再使用, 使用 MaterialInstance 和 MaterialBlock 代替")]
    public class MaterialProperties : IEnumerable<MaterialValue>
    {
        [SerializeField]
        List<MaterialValue> m_Values;

        public MaterialProperties(IList<MaterialValue> properties)
        {
            m_Values = new List<MaterialValue>(properties.Count);
            m_Values.AddRange(properties);
        }

        public MaterialProperties() { }

        public void UpdateMaterialPropertyId()
        {
            for (int i = 0; i < m_Values.Count; i++)
            {
                var v = m_Values[i];
                var id = string.IsNullOrEmpty(v.name) ? 0 : Shader.PropertyToID(v.name);
                if (id != v.id)
                {
                    v.id = id;
                    m_Values[i] = v;
                }
            }
        }

        IEnumerator<MaterialValue> IEnumerable<MaterialValue>.GetEnumerator()
        {
            return m_Values.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return m_Values.GetEnumerator();
        }

        public int Count { get { return m_Values == null ? 0 : m_Values.Count; } }

        public MaterialValue this[int index]
        {
            get { return m_Values[index]; }
        }

        public bool ContainsProperty(int propertyId)
        {
            if (m_Values == null)
                return false;
            for (int i = 0; i < m_Values.Count; i++)
            {
                if (m_Values[i].id == propertyId)
                    return true;
            }
            return true;
        }

        public void MergeProperties(MaterialProperties properties)
        {
            if (m_Values == null)
                m_Values = new List<MaterialValue>(properties.Count);
            for (int i = properties.Count - 1; i >= 0; i--)
            {
                var v = properties[i];
                if (!ContainsProperty(v.id))
                {
                    m_Values.Add(v);
                }
            }
        }

        public void MergeProperties(IList<MaterialValue> properties)
        {
            if (m_Values == null)
                m_Values = new List<MaterialValue>(properties.Count);
            for (int i = properties.Count - 1; i >= 0; i--)
            {
                var v = properties[i];
                if (!ContainsProperty(v.id))
                {
                    m_Values.Add(v);
                }
            }
        }

        public void SetMaterialPropertyBlock(MaterialPropertyBlock block)
        {
            var len = Count;
            for (int i = 0; i < len; i++)
            {
                SetValue(block, m_Values[i]);
            }
        }

        public void GetMaterialPropertyBlock(MaterialPropertyBlock block)
        {
            var len = Count;
            for (int i = 0; i < len; i++)
            {
                var v = m_Values[i];
                GetValue(block, ref v);
                m_Values[i] = v;
            }
        }
        public void GetMaterialProperty(Material mat)
        {
            var len = Count;
            for (int i = 0; i < len; i++)
            {
                var v = m_Values[i];
                GetValue(mat, ref v);
                m_Values[i] = v;
            }
        }

        public static void SetValue(MaterialPropertyBlock block, MaterialValue v)
        {
            if (v.type == EShaderPropertyType.Color || v.type == EShaderPropertyType.HdrColor)
                block.SetColor(v.id, v.value);
            else if (v.type == EShaderPropertyType.Float)
                block.SetFloat(v.id, v.value.x);
            else if (v.type == EShaderPropertyType.Vector)
                block.SetVector(v.id, v.value);
        }

        public static void GetValue(MaterialPropertyBlock block, ref MaterialValue v)
        {
            if (block.isEmpty)
                return;
            if (v.type == EShaderPropertyType.Color || v.type == EShaderPropertyType.HdrColor)
                v.value = block.GetColor(v.id);
            else if (v.type == EShaderPropertyType.Float)
                v.value = new Vector4(block.GetFloat(v.id), 0, 0, 0);
            else if (v.type == EShaderPropertyType.Vector)
                v.value = block.GetVector(v.id);
        }

        public static void GetValue(Material mat, ref MaterialValue v)
        {
            if (mat == null)
                return;
            if (v.type == EShaderPropertyType.Color || v.type == EShaderPropertyType.HdrColor)
                v.value = mat.GetColor(v.id);
            else if (v.type == EShaderPropertyType.Float)
                v.value = new Vector4(mat.GetFloat(v.id), 0, 0, 0);
            else if (v.type == EShaderPropertyType.Vector)
                v.value = mat.GetVector(v.id);
        }

        public static bool Blend(MaterialPropertyBlock block, MaterialProperties propA, MaterialProperties propB, float weightB, bool excludeNotSame = true)
        {
            if (block == null || propA == null || propB == null)
                return false;
            return Blend(block, propA.m_Values, propB.m_Values, weightB, excludeNotSame);
        }

        public static int IndexOfProperty(IList<MaterialValue> values, int propId)
        {
            if (values == null)
                return -1;
            for (int i = 0; i < values.Count; i++)
            {
                if (values[i].id == propId)
                    return i;
            }
            return -1;
        }

        /// <summary>
        /// 如果 properties 未排序，该方法不保证正确的混合结果
        /// </summary>
        /// <param name="block"></param>
        /// <param name="propA"></param>
        /// <param name="propB"></param>
        /// <param name="weightB"></param>
        /// <param name="excludeNotSame"></param>
        public static bool Blend(MaterialPropertyBlock block, IList<MaterialValue> propA, IList<MaterialValue> propB, float weightB, bool excludeNotSame = true)
        {
            var sizea = propA == null ? 0 : propA.Count;
            var sizeb = propB == null ? 0 : propB.Count;
            var dirty = false;
            cache.Clear();
            IList<MaterialValue> lsta, lstb;
            if (sizea >= sizeb)
            {
                lsta = propA;
                lstb = propB;
            }
            else
            {
                lsta = propB;
                lstb = propA;
                weightB = 1 - weightB;
                sizea = sizeb;
                sizeb = lstb == null ? 0 : lstb.Count;
            }
            for (int i = 0; i < sizea; i++)
            {
                var va = lsta[i];
                var n = IndexOfProperty(lstb, va.id);
                if (n != -1)
                {
                    cache.Add(va.id);
                    va.value = Vector4.Lerp(va.value, lstb[n].value, weightB);
                    SetValue(block, va);
                    dirty = true;
                }
                else if (!excludeNotSame)
                {
                    SetValue(block, va);
                    dirty = true;
                }
            }
            if (!excludeNotSame && sizeb > 0)
            {
                for (int i = 0; i < sizeb; i++)
                {
                    var vb = lstb[i];
                    if (!cache.Contains(vb.id))
                    {
                        SetValue(block, vb);
                        dirty = true;
                    }
                }
            }
            return dirty;
        }

        static HashSet<int> cache = new HashSet<int>();
    }
}